/*
 * Copyright (c) 2010, Nikolaus Moll
 * All rights reserved.
 */
package net.miblounge.gradecalculator.gui.models;

import java.util.ArrayList;
import java.util.List;

import net.miblounge.gradecalculator.core.AdaptedRegulations;
import net.miblounge.gradecalculator.core.descriptors.courses.Course;
import net.miblounge.gradecalculator.core.descriptors.courses.CourseDescription;
import net.miblounge.gradecalculator.core.descriptors.courses.IndividualCourse;
import net.miblounge.gradecalculator.core.descriptors.modulelistitems.IndividualModule;
import net.miblounge.gradecalculator.core.descriptors.modulelistitems.Module;
import net.miblounge.gradecalculator.core.helper.Helper;
import net.miblounge.gradecalculator.core.student.ExaminationResult;
import net.miblounge.gradecalculator.core.student.ExaminationResultType;
import net.miblounge.gradecalculator.core.student.StudentModule;
import net.miblounge.gradecalculator.grade.ICourseGrade;
import net.miblounge.gradecalculator.guicommon.exceptions.MultiColumnTreeModelException;
import net.miblounge.gradecalculator.guicommon.models.AbstractMultiColumnTreeModel;
import net.miblounge.gradecalculator.guicommon.widgets.TreeIndex;

import com.trolltech.qt.core.Qt;
import com.trolltech.qt.gui.QBrush;
import com.trolltech.qt.gui.QColor;
import com.trolltech.qt.gui.QFont;
import com.trolltech.qt.gui.QMessageBox;
import com.trolltech.qt.gui.QWidget;

public class GradesTreeModel extends AbstractMultiColumnTreeModel
{
    private static final int COLNAME = 0;
    private static final int COLECTS = 1;
    private static final int COLGRADES = 2;

    private static final String PASSED = "pass";

    private static final int ALRIGHT = Qt.AlignmentFlag.createQFlags(Qt.AlignmentFlag.AlignRight,
            Qt.AlignmentFlag.AlignVCenter).value();
    private static final int ALLEFT = Qt.AlignmentFlag.createQFlags(Qt.AlignmentFlag.AlignLeft,
            Qt.AlignmentFlag.AlignVCenter).value();

    private final AdaptedRegulations regulations;
    private final int gradeColumnCount;
    private final int columnCount;
    private final QFont fontModule;
    private final QFont fontCourse;

    private final QWidget parent;

    public GradesTreeModel(final QWidget parent, final AdaptedRegulations regulations)
    {
        this.regulations = regulations;
        this.parent = parent;
        registerMimeClass(ExaminationResult.class);

        fontModule = new QFont(parent.font().family(), parent.font().pointSize(), QFont.Weight.Bold.value(), false);
        fontCourse = new QFont(parent.font().family(), parent.font().pointSize(), QFont.Weight.Normal.value(), false);

        gradeColumnCount = ExaminationResultType.values().length;
        columnCount = COLGRADES + gradeColumnCount;
    }

    private int getModuleCount()
    {
        return regulations.getModules().size();
    }

    @Override
    public int getColumnCount()
    {
        return columnCount;
    }

    @Override
    public QFont getFont(final Object data)
    {
        if (data instanceof Course) {
            return fontCourse;
        }

        if (data instanceof ExaminationResult) {
            return fontCourse;
        }

        return fontModule;
    }

    @Override
    public QBrush getBrush(final Object data)
    {
        if (data instanceof StudentModule) {
            final StudentModule module = (StudentModule) data;
            final float reachedECTS = module.getECTS(ExaminationResultType.PASSED);
            final float moduleECTS = module.getModule().getECTS();
            return createBrush(reachedECTS, moduleECTS);
        }

        if (data instanceof Course) {
            final Course course = (Course) data;
            final ExaminationResult examiniationResult = regulations.getStudent().getResult(course);
            final Helper.PassResult passResult = Helper.getPassed(course, examiniationResult);
            return createBrush(passResult.ordinal(), Helper.PassResult.values().length);
        }

        return new QBrush(QColor.black);
    }

    protected static QBrush createBrush(final float value, final float max)
    {
        if (Math.abs(value - max) < 0.1) {
            return new QBrush(QColor.black);
        }

        final int minColor = 135;
        final int maxColor = 220;

        final float percent = value / max;
        final int color = 255 - (int) ((maxColor - minColor) * percent + minColor);

        return new QBrush(new QColor(color, color, color));
    }

    @Override
    public int getAlignment(final int column)
    {
        switch (column) {
        case COLECTS:
            return ALRIGHT;

        default:
            return ALLEFT;

        }
    }

    @Override
    public int getRowCount(final TreeIndex index)
    {
        if (index == null) {
            // root
            final int unassignedResultsRows;
            if (hasUnassignedResults()) {
                unassignedResultsRows = 1;
            }
            else {
                unassignedResultsRows = 0;
            }
            return 2 + getModuleCount() + unassignedResultsRows;
        }

        if (index.getParent() != null) {
            return 0;
        }

        if (isModuleRow(index.getRow())) {
            final StudentModule module = regulations.getModules().get(index.getRow() - 1);
            return module.getModule().getCourses().size();
        }

        if (isUnassignedResultRow(index.getRow())) {
            return regulations.getUnassignedResults().size();
        }

        return 0;
    }

    private boolean hasUnassignedResults()
    {
        return regulations.getUnassignedResults().size() > 0;
    }

    @Override
    public List<String> getHeaderLabels()
    {
        final List<String> result = new ArrayList<String>(getColumnCount());
        for (int i = 0; i < getColumnCount(); i++) {
            result.add(getHeaderText(i));
        }
        return result;
    }

    @Override
    public String getHeaderText(final int column)
    {
        switch (column) {
        case COLNAME:
            return "Name";

        case COLECTS:
            return "ECTS";

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            return type.toString();
        }
    }

    @Override
    public String getText(final Object data, final int column)
    {
        if (data instanceof StudentModule) {
            return getModuleText((StudentModule) data, column);
        }

        if (data instanceof IndividualCourse) {
            return getIndividualCourseText((IndividualCourse) data, column);
        }

        if (data instanceof Course) {
            return getCourseText((Course) data, column);
        }

        if (data instanceof ExaminationResult) {
            return getExaminationResultText((ExaminationResult) data, column);
        }

        if (data instanceof AdaptedRegulations) {
            return getOverallText(column);
        }

        return getUnassignedText(column);
    }

    private static String getExaminationResultData(final ExaminationResultType type, final ExaminationResult examRes)
    {
        if (type != ExaminationResultType.PASSED) {
            return "";
        }

        final String grade = examRes.getGrade(ExaminationResultType.PASSED);
        if (grade.equals("")) {
            return "pass";
        }
        return grade;
    }

    private String getExaminationResultText(final ExaminationResult examinationResult, final int column)
    {
        switch (column) {
        case COLNAME:
            if (examinationResult.getCourseDescription() == null) {
                return examinationResult.getCourseIdentifier();
            }
            return examinationResult.getCourseDescription().getName();

        case COLECTS:
            if (examinationResult.getCourseDescription() == null) {
                return "";
            }
            return Helper.formatECTS(examinationResult.getCourseDescription().getECTS());

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            // final ExaminationResult examinationResult =
            // regulations.getStudent().getResult(course.getExamNumber(),
            // course.getIdentifier());
            return getExaminationResultData(type, examinationResult);
        }
    }

    private String getModuleText(final StudentModule module, final int column)
    {
        switch (column) {
        case COLNAME:
            return module.getModule().getName();

        case COLECTS:
            return Helper.formatECTS(module.getModule().getECTS());

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            return getModuleData(type, module);
        }
    }

    private String getModuleData(final ExaminationResultType type, final StudentModule module)
    {
        final String ects;
        final float moduleECTS = module.getECTS(type);
        final boolean allECTS = (moduleECTS >= module.getModule().getECTS());
        if (!allECTS) {
            ects = " (" + Helper.formatECTS(moduleECTS) + ")";
        }
        else {
            ects = "";
        }

        final String grade;
        if (module.getModule().isGraded()) {
            grade = module.getGrade(type).toString();
        }
        else {
            if (allECTS) {
                grade = PASSED;
            }
            else {
                grade = "";
            }
        }

        final String result = grade + ects;

        if (type != ExaminationResultType.PASSED) {
            final String passedResult = getModuleData(ExaminationResultType.PASSED, module);
            if (passedResult.equals(result)) {
                return "";
            }
        }

        return result;
    }

    private static String getCourseData(final ExaminationResultType type, final Course course,
            final ExaminationResult examRes)
    {
        if (course.getUngradedPerformance().isEmpty() && course.getGradedPerformance().isEmpty()) {
            if (course instanceof IndividualCourse) {
                return "-";
            }
        }

        // if an ungraded performance is required, check if it is passed
        // TODO: think about returning passed grade in braces instead of empty
        // string
        // "(" grade ")"
        if (!course.getUngradedPerformance().isEmpty()) {
            if (!examRes.getUngradedPassed(type)) {
                return "";
            }
        }

        final String grade;
        if (!course.getGradedPerformance().isEmpty()) {
            grade = examRes.getGrade(type);
        }
        else {
            grade = PASSED;
        }

        if (type != ExaminationResultType.PASSED) {
            final String passed = getCourseData(ExaminationResultType.PASSED, course, examRes);
            if (passed.equals(grade)) {
                return "";
            }
        }

        return grade;
    }

    private String getCourseText(final Course course, final int column)
    {
        final ExaminationResult examinationResult = regulations.getStudent().getResult(course.getExamNumber(),
                course.getIdentifier());
        switch (column) {
        case COLNAME:
            if ((course instanceof IndividualCourse) && (examinationResult.getCourseDescription() != null)) {
                final CourseDescription desc = examinationResult.getCourseDescription();
                return course.getName() + ": " + desc.getName();
            }

            return course.getName();

        case COLECTS:
            return Helper.formatECTS(course.getECTS());

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            return getCourseData(type, course, examinationResult);
        }
    }

    private static String getIndividualCourseData(final ExaminationResultType type, final IndividualCourse course,
            final ExaminationResult examRes)
    {
        if (!examRes.getCourseDescription().getUngradedPerformance().isEmpty()) {
            if (!examRes.getUngradedPassed(type)) {
                return "";
            }
        }

        final String grade;
        if (!examRes.getCourseDescription().getGradedPerformance().isEmpty()) {
            grade = examRes.getGrade(type);
        }
        else {
            grade = PASSED;
        }

        if (type != ExaminationResultType.PASSED) {
            final String passed = getIndividualCourseData(ExaminationResultType.PASSED, course, examRes);
            if (passed.equals(grade)) {
                return "";
            }
        }

        return grade;
    }

    private String getIndividualCourseText(final IndividualCourse course, final int column)
    {
        final ExaminationResult examinationResult = regulations.getStudent().getResult(course.getExamNumber(),
                course.getIdentifier());
        if (examinationResult.getCourseDescription() == null) {
            return getCourseText(course, column);
        }

        switch (column) {
        case COLNAME:
            final CourseDescription desc = examinationResult.getCourseDescription();
            return course.getName() + ": " + desc.getName();

        case COLECTS:
            return Helper.formatECTS(course.getECTS());

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            return getIndividualCourseData(type, course, examinationResult);
        }
    }

    private String getOverallText(final int column)
    {
        switch (column) {
        case COLNAME:
            return "Overall";

        case COLECTS:
            return Helper.formatECTS(regulations.getSPOECTS());

        default:
            final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
            return getOverallData(type, regulations);
        }
    }

    private String getOverallData(final ExaminationResultType type, final AdaptedRegulations studentspo)
    {
        final float reachedECTS = studentspo.getECTS(type);
        final boolean allECTS = (reachedECTS >= studentspo.getSPOECTS());

        final String ects;
        if (!allECTS) {
            ects = " (" + Helper.formatECTS(reachedECTS) + ")";
        }
        else {
            ects = "";
        }

        final String grade = studentspo.getGrade(type).toString();
        final String result = grade + ects;

        if (type != ExaminationResultType.PASSED) {
            final String passedResult = getOverallData(ExaminationResultType.PASSED, studentspo);
            if (passedResult.equals(result)) {
                return "";
            }
        }

        return result;
    }

    private String getUnassignedText(final int column)
    {
        switch (column) {
        case COLNAME:
            return "Unassigned (" + regulations.getUnassignedResults().size() + ")";

        default:
            return "";
        }
    }

    @Override
    public boolean isInlineEditorAllowed(final TreeIndex index, final int column)
    {
        if (column < COLGRADES) {
            return false;
        }

        final Object data = getData(index);

        if (!(data instanceof Course)) {
            return false;
        }

        final Course course = (Course) data;

        if (!course.getGradedPerformance().isEmpty()) {
            // no graded performance => no setGrade
            return false;
        }

        final ExaminationResult result = regulations.getStudent().getResult(course);
        final boolean hasPassedGrade = !result.getGrade(ExaminationResultType.PASSED).equals("");
        final boolean isPassedGrade = (column == COLGRADES);

        if ((!isPassedGrade) && (hasPassedGrade)) {
            // no estimation allowed
            return false;
        }

        return true;
    }

    private boolean isModuleRow(final int row)
    {
        final int firstModuleRow = 1;
        final int lastModuleRow = getModuleCount();

        return ((row >= firstModuleRow) && (row <= lastModuleRow));
    }

    private boolean isOverallRow(final int row)
    {
        return ((row == 0) || (row == getModuleCount() + 1));
    }

    private boolean isUnassignedResultRow(final int row)
    {
        return (row == getModuleCount() + 2);
    }

    @Override
    protected <T> Object getChild(final T item, final int index)
    {
        throw new IllegalStateException("getChild is not implemented in this Model");
    }

    @Override
    public Object getData(final TreeIndex index)
    {
        if (index == null) {
            return null;
        }

        if (index.getParent() == null) {
            if (isOverallRow(index.getRow())) {
                return regulations;
            }

            if (isUnassignedResultRow(index.getRow())) {
                return null;
            }

            if (isModuleRow(index.getRow())) {
                return regulations.getModules().get(index.getRow() - 1);
            }

            return null;
        }

        final int parentRow = index.getParent().getRow();
        if (isModuleRow(parentRow)) {
            // Course
            final int moduleIndex = index.getParent().getRow() - 1;
            final StudentModule module = regulations.getModules().get(moduleIndex);
            return module.getModule().getCourses().get(index.getRow());
        }

        if (isUnassignedResultRow(parentRow)) {
            return regulations.getUnassignedResults().get(index.getRow());
        }

        return null;
    }

    private void setGrade(final TreeIndex index, final int column, final String text, final Course course)
    {
        if (column < COLGRADES) {
            throw new MultiColumnTreeModelException("Only editing grades is allowed");
        }
        if (!course.getGradedPerformance().isEmpty()) {
            // no graded performance => no setGrade
            return;
        }

        final ExaminationResult result = regulations.getStudent().getResult(course);
        final ExaminationResultType type = ExaminationResultType.values()[column - COLGRADES];
        final boolean hasPassedGrade = !result.getGrade(ExaminationResultType.PASSED).equals("");
        final boolean isPassedGrade = (column == COLGRADES);

        if ((!isPassedGrade) && (hasPassedGrade)) {
            // no estimation allowed
            return;
        }

        final String newGrade;
        if ((text == null) || text.equals("")) {
            newGrade = "";
        }
        else {
            if (!regulations.getGradeFactory().getValidator().isValid(text)) {
                throw new MultiColumnTreeModelException("'" + text + "' is not a valid grade");
            }

            final ICourseGrade grade = regulations.getGradeFactory().createCourseGrade(text, 1);
            newGrade = grade.getGrade();
        }

        final String oldGrade = result.getGrade(type);

        if (oldGrade.equals(text)) {
            return;
        }

        result.setGrade(type, newGrade);

        notifyDataChanged(index, column, newGrade);
        updateOveralls();
    }

    @Override
    public void setText(final TreeIndex index, final int column, final String text)
    {
        final Object data = getData(index);
        if (data instanceof Course) {
            setGrade(index, column, text, (Course) data);
        }
    }

    private void updateOveralls()
    {
        notifyDataChanged(new TreeIndex(0));
        notifyDataChanged(new TreeIndex(getModuleCount() + 1));
        if (hasUnassignedResults()) {
            notifyDataChanged(new TreeIndex(getModuleCount() + 2));
        }
    }

    private StudentModule getStudentModule(final Module module)
    {
        for (final StudentModule studentModule : regulations.getModules()) {
            if (studentModule.getModule() == module) {
                return studentModule;
            }
        }

        return null;
    }

    public void addCourse(final CourseDescription description)
    {
        final IndividualModule module = description.getParentModule();
        final StudentModule studentModule = getStudentModule(module);
        if (studentModule == null) {
            throw new IllegalStateException("Illegal module (" + module + ")");
        }
        final int modelIndex = regulations.getModules().indexOf(studentModule);
        module.addCourse(description);

        final int parentRow = modelIndex + 1;

        final TreeIndex index = new TreeIndex(parentRow);

        notifyChildInserted(index, getRowCount(index) - 1);
    }

    public void removeCourse(final CourseDescription description)
    {
        final IndividualModule module = description.getParentModule();
        final StudentModule studentModule = getStudentModule(module);
        if (studentModule == null) {
            throw new IllegalStateException("Illegal module (" + module + ")");
        }

        final int modelIndex = regulations.getModules().indexOf(studentModule);
        final int parentRow = modelIndex + 1;
        final TreeIndex index = new TreeIndex(parentRow);

        final int courseRow = module.getCourses().indexOf(description);

        module.removeCourse(description);
        notifyChildRemoved(index, courseRow);
    }

    // DRAG & DROP
    @Override
    public boolean isDragEnabled()
    {
        return true;
    }

    @Override
    public boolean isDropEnabled()
    {
        return true;
    }

    @Override
    public boolean isDropIndicatorShown()
    {
        return true;
    }

    @Override
    public boolean isDragAllowed(final TreeIndex index, final Object data)
    {
        return (data instanceof ExaminationResult);
    }

    @Override
    public boolean isDropAllowed(final Object draggingObject, final TreeIndex index, final Object target)
    {
        if (target == null) {
            return false;
        }

        if (draggingObject instanceof ExaminationResult) {
            if (target instanceof IndividualCourse) {
                return false;
            }

            if (target instanceof StudentModule) {
                final StudentModule studentModule = (StudentModule) target;
                return (studentModule.getModule() instanceof IndividualModule);
            }

            if (target instanceof CourseDescription) {
                return true;
            }

            if (target instanceof Course) {
                return true;
            }

            return false;
        }

        return false;
    }

    @Override
    public void dropItem(final TreeIndex sourceIndex, final Object draggingObject, final TreeIndex targetIndex,
            final Object target)
    {
        if (draggingObject instanceof ExaminationResult) {
            dropItem(sourceIndex, (ExaminationResult) draggingObject, targetIndex, target);
        }
    }

    public void dropItem(final TreeIndex sourceIndex, final ExaminationResult draggingObject,
            final TreeIndex targetIndex, final Object target)
    {
        if (target instanceof StudentModule) {
            dropItem(sourceIndex, draggingObject, targetIndex, (StudentModule) target);
        }

        if (target instanceof Course) {
            dropItem(sourceIndex, draggingObject, targetIndex, (Course) target);
        }
    }

    public void dropItem(final TreeIndex sourceIndex, final ExaminationResult examinationResult,
            final TreeIndex targetIndex, final StudentModule target)
    {
        if (target.getModule() instanceof IndividualModule) {
            final IndividualModule module = (IndividualModule) target.getModule();
            final CourseDescription course = examinationResult.getCourseDescription();
            course.setModuleName(module.getName());
            examinationResult.setCourseIdentifier(module.getIdentifier() + "." + course.getName());
            module.addCourse(course);

            final int courseRow = module.getCourses().indexOf(course);
            regulations.removeUnassignedResult(examinationResult);
            regulations.getStudent().addResult(examinationResult);

            final boolean removeUnassigned = !hasUnassignedResults();

            final TreeIndex newChildIndex = new TreeIndex(courseRow, targetIndex);
            notifyChildMoved(sourceIndex, newChildIndex);
            if (removeUnassigned) {
                notifyChildRemoved(null, getModuleCount() + 2);
            }
        }

        // TOOD instanceof Module
    }

    public void dropItem(final TreeIndex sourceIndex, final ExaminationResult examinationResult,
            final TreeIndex targetIndex, final CourseDescription neighbour)
    {
        final IndividualModule module = neighbour.getParentModule();

        // TODO reduce redundancy with (target.getModule() instanceof
        // IndividualModule)-code
        final CourseDescription course = examinationResult.getCourseDescription();
        course.setModuleName(module.getName());
        examinationResult.setCourseIdentifier(module.getIdentifier() + "." + course.getName());
        module.addCourse(course);

        final int courseRow = module.getCourses().indexOf(course);
        regulations.removeUnassignedResult(examinationResult);
        regulations.getStudent().addResult(examinationResult);

        final boolean removeUnassigned = !hasUnassignedResults();

        final TreeIndex newChildIndex = new TreeIndex(courseRow, targetIndex.getParent());
        notifyChildMoved(sourceIndex, newChildIndex);
        if (removeUnassigned) {
            notifyChildRemoved(null, getModuleCount() + 2);
        }
    }

    public void dropItem(final TreeIndex sourceIndex, final ExaminationResult examinationResult,
            final TreeIndex targetIndex, final Course course)
    {
        if (course instanceof CourseDescription) {
            dropItem(sourceIndex, examinationResult, targetIndex, (CourseDescription) course);
            return;
        }

        // set course result
        // TODO fuzzy compare

        boolean query = false;
        if (examinationResult.getCourseDescription() != null) {
            if (!course.getName().equals(examinationResult.getCourseDescription().getName())) {
                query = true;
            }
            else if (Math.abs(course.getECTS() - examinationResult.getCourseDescription().getECTS()) > 0.01) {
                query = true;
            }
        }
        else {
            query = true;
        }

        if (query) {
            // message box
            final QMessageBox.StandardButtons buttons = new QMessageBox.StandardButtons();
            buttons.set(QMessageBox.StandardButton.Ok);
            buttons.set(QMessageBox.StandardButton.Cancel);

            final QMessageBox.StandardButton ret = QMessageBox.warning(parent, parent.tr("Grade Calculator"),
                    parent.tr("Course information do not match. Continue anyway?"), buttons,
                    QMessageBox.StandardButton.Ok);
            if (ret == QMessageBox.StandardButton.Cancel) {
                return;
            }
        }

        final ExaminationResult oldResult = regulations.getStudent().getResult(course);
        examinationResult.setCourseDescription(null);
        examinationResult.setCourseIdentifier(course.getIdentifier());
        regulations.getStudent().removeResult(oldResult);
        regulations.getStudent().addResult(examinationResult);
        regulations.removeUnassignedResult(examinationResult);

        final boolean removeUnassigned = !hasUnassignedResults();

        notifyDataChanged(targetIndex);
        notifyChildRemoved(sourceIndex.getParent(), sourceIndex.getRow());
        if (removeUnassigned) {
            notifyChildRemoved(null, getModuleCount() + 2);
        }
    }

    @Override
    public boolean isRecursiveUpdateAllowed(final TreeIndex row)
    {
        if (row.getParent() != null) {
            return true;
        }

        return isModuleRow(row.getRow());
    }

    @Override
    public void recursiveUpdate(final TreeIndex row)
    {
        if (row == null) {
            updateOveralls();
            return;
        }

        if ((row.getParent() != null) || (isModuleRow(row.getRow()))) {
            super.recursiveUpdate(row);
        }

    }

    public void removeExaminationResult(final ExaminationResult examinationResult)
    {
        final int row = regulations.getUnassignedResults().indexOf(examinationResult);
        regulations.removeUnassignedResult(examinationResult);
        regulations.getStudent().removeResult(examinationResult);

        notifyChildRemoved(new TreeIndex(getModuleCount() + 2), row);

        if (regulations.getUnassignedResults().size() == 0) {
            notifyChildRemoved(null, getModuleCount() + 2);
        }
    }
}
